local AddOnName, Env = ...; local ADDON, DEV = Env[1], Env[1].DEV
-- | Libraries |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
local LibBoss = LibStub("LibBossIDs-1.0")
-- | Lua Upvalues |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
local tonumber, type							= tonumber,type
local remove											= table.remove
-- | WoW Upvalues |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
local GetTime, wipe																		= GetTime, wipe
local GetInventoryItemCooldown, GetSpellCooldown			= GetInventoryItemCooldown, GetSpellCooldown
local UnitName, UnitDebuff, UnitIsVisible, UnitExists	= UnitName, UnitDebuff, UnitIsVisible, UnitExists
local UnitLevel, UnitGUID															= UnitLevel, UnitGUID
local GetSpellInfo, GetSpellLink											= GetSpellInfo, GetSpellLink
local GetSpellTabInfo, GetNumSpellTabs 								= GetSpellTabInfo, GetNumSpellTabs
local GetSpellBookItemInfo, GetSpellBookItemName			= GetSpellBookItemInfo, GetSpellBookItemName
local IsSpellInRange 																	= IsSpellInRange
local RunMacroText																		= RunMacroText
local IsUsableSpell																		= IsUsableSpell
local GetRuneCooldown																	= GetRuneCooldown
-- | FireHack Upvalues |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


-- | Variables |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
local KEY_VALUES 	= {
  ls = 1,
  lc = 2,
  la = 4,
  rs = 8,
  rc = 16,
  ra = 32,
}
local KEYS = {
	ls = function() return IsLeftShiftKeyDown() end,
  lc = function() return IsLeftControlKeyDown() end,
  la = function() return IsLeftAltKeyDown() end,
  rs = function() return IsRightShiftKeyDown() end,
 	rc = function() return IsRightControlKeyDown() end,
  ra = function() return IsRightAltKeyDown() end,
}
local LUST_BUFFS = {
	32182,	-- Heroism
	2825,		-- Bloodlust
	80353,	-- Time Warp
	90355,	-- Ancient Hysteria
}

-- FireHack
local FH_Object1, FH_Object2, FH_Object3, FH_Object4, FH_Object5
local FH_PlayerObject

-- Engine
local rotationUpdate
local interruptUpdate

local activeEngine
local activeRotation
local activeRotationNumber
local prevRotationNumber
local activeMode
local prevMode
local interruptOn
local activeInterrupt
local prevInterrupt
local rotationElapsed			= 0
local interruptElapsed		= 0
local spellRecast 				= {}
local spellRecastDelay 		= {}
local spellAvailableTime 	= 0.100
local lagTolerance				= .2 -- 200ms
-- Spell Logging
local executeCount
local executedAction
local prevLogAction
local prevLogRotation
local prevLogMode
local prevExecutedRotation
local prevExecutedAction
local actionLog = setmetatable({},{__index = {
	['Log'] = function(self,action)
		self[#self+1] = action
		if #self >= 200 then remove(self,1) end
		ADDON.ActionLog:Refresh()
	end,
	['AddLineID'] = function(self,spell,time,lineID)
		for i = #self, 1, -1 do
			if self[i].time == time then
				if self[i].spell == spell then
					self[i].lineID = lineID
					ADDON.ActionLog:Refresh()
				return end
			elseif self[i].time < time then return end
		end
	end,
	['MarkSuccess'] = function(self,lineID, spell)
		for i = #self, 1, -1 do
			if self[i].lineID == lineID then
				if self[i].spell == spell then self[i].success = true end
			return end
		end
	end,
}})

local spellBook = setmetatable({ [BOOKTYPE_SPELL] = {}, [BOOKTYPE_PET] = {}, flyout = {}},{__index = {	
	['Update'] = function(self)
		wipe(self[BOOKTYPE_SPELL])
		wipe(self[BOOKTYPE_PET])
		wipe(self.flyout)

		self:UpdateBook(BOOKTYPE_SPELL)
		self:UpdateBook(BOOKTYPE_PET)

		for f = 1, GetNumFlyouts() do
			local flyoutID = GetFlyoutID(f)
			local _, _, numSlots, isKnown = GetFlyoutInfo(flyoutID)
			if isKnown and numSlots > 0 then
				for s = 1, numSlots do
					local spellID, _, isKnownSpell = GetFlyoutSlotInfo(flyoutID, s)
					local name = GetSpellInfo(spellID)
					if name and isKnownSpell then
						self.flyout[name] = true
					end
				end
			end
	  	end

	end,
	['UpdateBook'] = function(self,bookType)
		local _, _, offset, numspells = GetSpellTabInfo(GetNumSpellTabs())
		local book = self[bookType]

		for bookID = 1, offset + numspells do
			local type, spellID = GetSpellBookItemInfo(bookID, bookType)

			if type == "SPELL" then
				local SpellBookItemName = GetSpellBookItemName(bookID, bookType)
				local baseSpellName = GetSpellInfo(spellID)

				if SpellBookItemName then book[SpellBookItemName] = bookID end
				if baseSpellName then book[baseSpellName] = bookID end
			end
		end
	end,
	['GetSpellIndex'] = function(self, spell)
		local spellID = tonumber(spell)
		if spellID then spell = GetSpellInfo(spellID) end

		local index = self[BOOKTYPE_SPELL][spell]
		if index then return index, BOOKTYPE_SPELL end
		index = self[BOOKTYPE_PET][spell]
		if index then return index, BOOKTYPE_PET end
		index = self.flyout[spell]
		if index then return index end
	end,
}})

local playerTalents			= {}
-- | local functions|~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

local function resetRecastDelay(spellID)
	if spellID == nil or spellID == 0 or not spellRecast[spellID] then return end
	spellRecastDelay[spellID] = 0
	ADDON:Debug("rotEngine","resetRecastDelay called for spell: "..GetSpellInfo(spellID))
end
local function setRecastDelay(spellID, addCastTime)
	if spellID == nil or spellID == 0 or not spellRecast[spellID] then return end
	local castingSpellName,_, _, _, _, _, castTime = GetSpellInfo(spellID)
	if not castingSpellName then return end
	if addCastTime then
		spellRecastDelay[spellID] = (GetTime()*1000) + spellRecast[spellID] + castTime
	else
		spellRecastDelay[spellID] = (GetTime()*1000) + spellRecast[spellID]
	end
	ADDON:Debug("rotEngine","SetRecastDelay called for: "..castingSpellName)
end
-- | Engine |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
local function Engine_OnUpdate(self, elapsed)	
	-- Rotation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	rotationElapsed = rotationElapsed + elapsed
	while (rotationElapsed > rotationUpdate) do
		rotationElapsed = rotationElapsed - rotationUpdate

		if activeRotation then -- Run Engine
			if prevRotation ~= activeRotation or prevMode ~= activeMode then -- start/change Rotation
				ADDON.Remote:SetStatus(activeMode.."Start",{ADDON.Rotations[activeRotationNumber].data.name,'<No Action Ready>'})
				ADDON:Debug("rotEngine","Rotation: "..activeRotationNumber.." in mode: "..activeMode.." Enabled.")
			end
			activeEngine = 'rotation'
			activeRotation()
		elseif prevRotation then -- Stop Rotation
			ADDON.Remote:SetStatus('ready')
			ADDON:Debug("rotEngine","Rotation: "..prevRotationNumber.." in mode: "..prevMode.." Disabled.")
		end

		prevRotationNumber 	= activeRotationNumber
		prevRotation 				= activeRotation
		prevMode		 				= activeMode
	end
	-- Interrupt ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	interruptElapsed = interruptElapsed + elapsed
	while (interruptElapsed > interruptUpdate) do
		interruptElapsed = interruptElapsed - interruptUpdate

		if activeInterrupt then -- Run Rotation
			if prevInterrupt ~= activeInterrupt then -- start Rotation
				ADDON.Remote:SetInterrupt(true)
				ADDON:Debug("rotEngine","Interrupt Enabled.")
			end
			activeEngine = 'interrupt'
			activeInterrupt()
		elseif prevInterrupt then -- Stop Rotation
			ADDON.Remote:SetInterrupt(false)
			ADDON:Debug("rotEngine","Interrupt Disabled.")
		end

		prevInterrupt = activeInterrupt
	end
end
-- | Rotation Engine Methods |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
local methods = {
	['Update'] = function(self)		
		self:SetRotationUpdate(self.db.rotationUpdate)
		self:SetInterruptUpdate(self.db.interruptUpdate)		
	end,
	['OnFireHack'] = function(self)			
		local newIndex = {} 
		local playerMT = getmetatable(Player) -- remove the inifinte recursion in the metatable
		for k, v in pairs(playerMT) do
		  if k ~= '__index' then newIndex[k] = v end		  
		end
	
		FH_PlayerObject = setmetatable({GUID=Player.GUID},{__index=newIndex})
		-- localize a set of firehack objects to use rather then creating a new one everytimne 
		
		FH_Object1 = setmetatable({},{__index=newIndex})			
		FH_Object2 = setmetatable({},{__index=newIndex})	
		
		
	end,
	['StopRotationEngine'] = function(self)
		activeRotation, activeRotationNumber, activeMode = nil, nil, nil
	end,
	['StopInterruptEngine'] = function(self)
		activeInterrupt = nil
	end,
	['SetRotationUpdate'] = function(self,num)
		rotationUpdate = num
	end,
	['SetInterruptUpdate'] = function(self,num)
		interruptUpdate = num
	end,
	['InputHandler'] = function(self,rotationNumber,mode,down)
		if rotationNumber < 6 then
			if mode == 'manual' then
				if down then
					activeRotationNumber, activeMode	= rotationNumber, mode
					activeRotation = ADDON.Rotations[rotationNumber].rotation
				else -- Up
					if activeRotationNumber == rotationNumber and activeMode == 'manual' then
						self:StopRotationEngine()
					end
				end
			else -- auto
				if not activeRotationNumber or activeRotationNumber ~= rotationNumber or activeMode == 'manual'  then
					activeRotationNumber, activeMode	= rotationNumber, mode
					activeRotation = ADDON.Rotations[rotationNumber].rotation
				elseif activeMode == 'auto' and activeRotationNumber == rotationNumber then
					self:StopRotationEngine()
				end
			end

		else -- Interrupt
			interruptOn = not interruptOn
			if interruptOn then
				activeInterrupt = ADDON.Rotations[6].rotation
			else
				self:StopInterruptEngine()
			end
		end
	end,
	['LogAction'] = function(self,action,spellID)
		local activeRotationNumber = activeRotationNumber
		local activeMode = activeMode

		if activeEngine == 'interrupt' then
			activeRotationNumber = 6
			activeMode = 'auto'
			executedAction = {
			 	action				= action,
			 	spellID 				= spellID,
			 	author				= ADDON.Rotations[6].data.author,
				spell					= GetSpellInfo(spellID),
				time					= GetTime(),
				mode					= activeMode,
			 	rotation				= ADDON.Rotations[6].data.name,
			}
		else
			executedAction = {
			 	action				= action,
			 	spellID 				= spellID,
			 	author				= ADDON.Rotations[activeRotationNumber].data.author,
				spell					= GetSpellInfo(spellID),
				time					= GetTime(),
			 	mode					= activeMode,
			 	rotation				= ADDON.Rotations[activeRotationNumber].data.name,
			}
		end
		actionLog:Log(executedAction)

		if prevLogAction	== action and prevLogRotation == activeRotationNumber and prevLogMode == activeMode then
			executeCount = executeCount + 1
		else
			executeCount = 1
		end
		executedAction.executeCount = executeCount
		ADDON.Remote:SetStatus('action',executedAction)

		prevLogAction			= action
		prevLogRotation		= activeRotationNumber
		prevLogMode				= activeMode
	end,	
	['UNIT_SPELLCAST_SENT'] = function(self, unitID, spell, rank, target, lineID)
		if unitID ~= "player" then return end
		local time = GetTime()
		actionLog:AddLineID(spell, time, lineID)
	end,
	['UNIT_SPELLCAST_SUCCEEDED'] = function(self, unitID, spell, rank, lineID, spellID)	-- unitCaster, unitSpell, _, _, unitSpellID
		if unitID ~= "player" then return end
		setRecastDelay(unitSpellID)
		local action = {
			action 		= 'SPELLCAST_SUCCEEDED',
			spellID 		= spellID,
			spell			= spell,
			lineID		= lineID,
			time			= GetTime(),
		}
		actionLog:MarkSuccess(lineID, spell)
		actionLog:Log(action)
	end,
	['UNIT_SPELLCAST_START'] = function(self,...)

	end,
	['UNIT_SPELLCAST_INTERRUPTED'] = function(self,...)
		if unitCaster ~= "player" then return end
		resetRecastDelay(unitSpellID)
	end,
	['UNIT_SPELLCAST_FAILED'] = function(self,...)

	end,
	['UNIT_SPELLCAST_FAILED_QUIET'] = function(self,...)
		self['UNIT_SPELLCAST_FAILED'](self,...)
	end,
	['SPELLS_CHANGED'] = function(self,...)
		spellBook:Update()
	end,
	['PLAYER_TALENT_UPDATE'] = function(self,...)
		wipe(playerTalents)

		for i = 1, GetMaxTalentTier() do
			local talent = select(2,GetTalentRowSelectionInfo(i))
			if talent then
				playerTalents[talent] = true
			end
		end
	end,
}
-- | Engine Constructor |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
function ADDON:constructEngine()
	local self = CreateFrame('Frame')
	self.actionLog = actionLog
	self.db = ADDON.db.profile.engine

	self:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED")
	self:RegisterEvent("UNIT_SPELLCAST_START")
	self:RegisterEvent("UNIT_SPELLCAST_SENT")
	self:RegisterEvent("UNIT_SPELLCAST_INTERRUPTED")
	self:RegisterEvent("UNIT_SPELLCAST_FAILED")
	self:RegisterEvent("UNIT_SPELLCAST_FAILED_QUIET")
	self:RegisterEvent("SPELLS_CHANGED")
	self:RegisterEvent("PLAYER_TALENT_UPDATE")
-- 	self:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
-- 	self:RegisterEvent("UI_ERROR_MESSAGE")
	self:SetScript("OnUpdate", Engine_OnUpdate)
	self:SetScript("OnEvent", function(this, event, ...) self[event](self,...) end)
	-- ~~ Methods ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	for method, func in pairs(methods) do
		self[method] = func
	end
	-- Trigger Events ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	-- self:PLAYER_TALENT_UPDATE()
	self:SPELLS_CHANGED()
	-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	return self
end
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-- | Rotation API |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

--[[ unitID = GetUnitID(unit)
-- @param	unit - A unit to query (string, unitID)
	@Return	unitID - ID of unit, nil otherwise. ]]
local function GetUnitID(unit)
	if not UnitGUID(unit) then return end
	return tonumber(UnitGUID(unit):sub(-13, -9), 16)
end

-- FIREHACK ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
local function FaceUnit(unit)
	local guid = UnitGUID(unit)
	if guid then
		FH_Object1.GUID = guid
		FH_Object1:Face()
	end	
end
--[[ distance = GetDistance("unit")
	@param	unit - A unit to query (string, unitID)
	@Return	distance - distance to unit, nil otheriwse (number) ]]
local function GetDistance(unit)
	local guid = UnitGUID(unit)
	if guid then
		FH_Object1.GUID = guid		
		return FH_Object1:GetDistance()
	end		
end
-- COMBAT ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
--[[ combat = InCombat()
	@Return	combat - true if in combat; otherwise false (bool) ]]
local function InCombat()
	local players = GetNumGroupMembers()
	if players then
		if IsInRaid("PLAYER") then
			for i = 1, players do if UnitAffectingCombat("RAID".. i) then return true end end
		else
			for i = 1, players do if UnitAffectingCombat("PARTY".. i) then return true end end
		end
		if UnitAffectingCombat("PLAYER") then return true end
	else
		if UnitAffectingCombat("PLAYER") then return true end
	end
	return false
end
--[[ boss = IsBoss("unit")
	@param	unit - A unit to query (string, unitID)
	@Return	boss - true if the unit is a boss; otherwise false (bool) ]]
local function IsBoss(unit)
	local classification = UnitClassification(unit)
  	if classification == 'rareelite' or classification == 'rare' then return true end
	if classification == 'worldboss' or LibBoss[GetUnitID(unit)] then return true end
 	return false
end

-- TALENTS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
--[[ active = TalentActive(talent)
-- @param	talent - Index of talent to check for. (number)
	@Return	active - true if the player has talent; otherwise false (bool) ]]
local function TalentActive(talent)
	return playerTalents[talent] or false
end

-- SPELLS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
--[[ inRange = SpellInRange("spellname" or spellID, "unit")
-- @param	spell - Numeric ID of a spell (number) or Name of a spell (string)
	@param	unit - A unit to query (string, unitID)
	@Return	inRange - 1 if the player is near enough to cast the spell on the unit; 0 if not in range; nil if the unit is not a valid target for the spell ]]
local function SpellInRange(spell, unit)
	local spellID = tonumber(spell)
	if spellID then spell = GetSpellInfo(spellID) end

	local index, spellBook = spellBook:GetSpellIndex(spell)
	if spellBook then return IsSpellInRange(index, spellBook, unit) end

	return IsSpellInRange(spell, unit)
end
--[[ available = SpellAvailable(spellID)
	@param	spellID - Numeric ID of a spell (number)
	@Return	available - true if the spell is available; otherwise false (bool) ]]
local function SpellAvailable(spellID)
	local gcdStartTime, gcdDuration 		= GetSpellCooldown(61304) -- GCD
	local spellStartTime, spellDuration = GetSpellCooldown(spellID)
	if not IsUsableSpell(spellID) or not spellStartTime or not gcdDuration then return false end

	local spellTimeLeft = spellStartTime + spellDuration - GetTime()
	if gcdDuration == 0 then --  no GCD
		if spellTimeLeft <= spellAvailableTime then return true end
	elseif gcdDuration > 0 then -- in GCD
		if spellTimeLeft <= lagTolerance then return true end
	end
	return false
end
--[[ time = SpellCooldownTime(spellID)
	@param	spellID - Numeric ID of a spell (number)
	@Return	time - time left on cooldown in seconds, 0 if ready. (number) ]]
local function SpellCooldownTime(spellID)
	local start, duration, enabled = GetSpellCooldown(spellID)
	if start ~= 0 then return ( start + duration - GetTime() ) end
	return 0
end
--[[ channeling = SpellChanneling(unit)
	@param	unit - A unit to query (string, unitID)
	@Return	channeling - true if the unit is channeling; otherwise false (bool) ]]
local function SpellChanneling(unit)
	local _, _, _, _, _, channeling = UnitChannelInfo(unit)
	if channeling and (channeling - (GetTime()*1000)) > 100 then
		return true
	end -- 100ms
	return false
end

-- AURAS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
--[[ name, rank, icon, count, dispelType, duration, expires, caster, isStealable, shouldConsolidate, spellID, canApplyAura, isBossDebuff, value1, value2, value3 = UnitDebuffID("unit", spellID [, "filter"])
	@param	unit - A unit to query (string, unitID)
	@param	spellID - Numeric ID of a spell (number)
	@param	filter - list of filters to use separated by the pipe '|' character; e.g. "CANCELABLE|PLAYER" will query cancelable debuffs cast by the player (string)
	@Return
		name - Name of the aura (string)
		rank - Secondary text for the aura (often a rank; e.g. "Rank 7") (string)
		icon - Path to an icon texture for the aura (string)
		count - The number of times the aura has been applied (number)
		dispelType - Type of aura (relevant for dispelling and certain other mechanics); nil if not one of the following values: (string)
			Curse
			Disease
			Magic
			Poison
		duration - Total duration of the aura (in seconds) (number)
		expires - Time at which the aura will expire; can be compared to GetTime() to determine time remaining (number)
		caster - Unit which applied the aura. If the aura was applied by a unit that does not have a token but is controlled by one that does (e.g. a totem or another player's vehicle), returns the controlling unit. Returns nil if thecasting unit (or its controller) has no unitID. (string, unitID)
		isStealable - 1 if the aura can be transferred to a player using the Spellsteal spell; otherwise nil (1nil)
		shouldConsolidate - 1 if the aura is eligible for the 'consolidated' aura display in the default UI. (1nil)
		spellID - spellID of the aura (number)
		canApplyAura - 1 if the player can apply the aura (not necessarily if the player did apply the aura, just if the player can apply the aura). (1nil)
		isBossDebuff - 1 if the aura was cast by a boss. (1nil)
		value1 - Value of variable effect 1 of the aura. (HoTs, resource-capturing trinkets, etc.) (number)
		value2 - Value of variable effect 2 of the aura. (HoTs, resource-capturing trinkets, etc.) (number)
		value3 - Value of variable effect 3 of the aura. (HoTs, resource-capturing trinkets, etc.) (number) ]]
local function UnitDebuffID(unit, spellID, filter)
	return UnitDebuff(unit, GetSpellInfo(spellID), nil, filter)
end
--[[ time = UnitDebuffTime("unit", spellID[, "filter"])
	@param	unit - A unit to query (string, unitID)
	@param	spellID - Numeric ID of a spell (number)
	@param	filter - list of filters to use separated by the pipe '|' character; e.g. "CANCELABLE|PLAYER" will query cancelable debuffs cast by the player (string)
	@Return	time - time left on debuff in seconds, 0 otherwise. (number)]]
local function UnitDebuffTime(unit,spellID,filter)
	local _,_,_,_,_,_,debuffExpire = UnitDebuffID(unit, spellID, filter)
	return debuffExpire and debuffExpire - GetTime() or 0
end
--[[ stacks = UnitDebuffStacks("unit", spellID[, "filter"])
	@param	unit - A unit to query (string, unitID)
	@param	spellID - Numeric ID of a spell (number)
	@param	filter - list of filters to use separated by the pipe '|' character; e.g. "CANCELABLE|PLAYER" will query cancelable debuffs cast by the player (string)
	@Return	stacks - stack count of debuff, 0 otheriwse. (number)]]
local function UnitDebuffStacks(unit,spellID,filter)
	local _,_,_,s = UnitDebuffID(unit, spellID, filter)
	return s and s or 0
end
--[[ name, rank, icon, count, dispelType, duration, expires, caster, isStealable, shouldConsolidate, spellID, canApplyAura, isBossDebuff, value1, value2, value3 = UnitBuffID("unit", spellID [, "filter"])
	@param	unit - A unit to query (string, unitID)
	@param	spellID - Numeric ID of a spell (number)
	@param	filter - list of filters to use separated by the pipe '|' character; e.g. "CANCELABLE|PLAYER" will query cancelable debuffs cast by the player (string)
	@Return
		name - Name of the aura (string)
		rank - Secondary text for the aura (often a rank; e.g. "Rank 7") (string)
		icon - Path to an icon texture for the aura (string)
		count - The number of times the aura has been applied (number)
		dispelType - Type of aura (relevant for dispelling and certain other mechanics); nil if not one of the following values: (string)
			Curse
			Disease
			Magic
			Poison
		duration - Total duration of the aura (in seconds) (number)
		expires - Time at which the aura will expire; can be compared to GetTime() to determine time remaining (number)
		caster - Unit which applied the aura. If the aura was applied by a unit that does not have a token but is controlled by one that does (e.g. a totem or another player's vehicle), returns the controlling unit. Returns nil if thecasting unit (or its controller) has no unitID. (string, unitID)
		isStealable - 1 if the aura can be transferred to a player using the Spellsteal spell; otherwise nil (1nil)
		shouldConsolidate - 1 if the aura is eligible for the 'consolidated' aura display in the default UI. (1nil)
		spellID - spellID of the aura (number)
		canApplyAura - 1 if the player can apply the aura (not necessarily if the player did apply the aura, just if the player can apply the aura). (1nil)
		isBossDebuff - 1 if the aura was cast by a boss. (1nil)
		value1 - Value of variable effect 1 of the aura. (HoTs, resource-capturing trinkets, etc.) (number)
		value2 - Value of variable effect 2 of the aura. (HoTs, resource-capturing trinkets, etc.) (number)
		value3 - Value of variable effect 3 of the aura. (HoTs, resource-capturing trinkets, etc.) (number) ]]
local function UnitBuffID(unit, spellID, filter)
	return UnitBuff(unit, GetSpellInfo(spellID), nil, filter)
end
--[[ time = UnitBuffTime("unit", spellID[, "filter"])
	@param	unit - A unit to query (string, unitID)
	@param	spellID - Numeric ID of a spell (number)
	@param	filter - list of filters to use separated by the pipe '|' character; e.g. "CANCELABLE|PLAYER" will query cancelable debuffs cast by the player (string)
	@Return	time - time left on buff in seconds, 0 otherwise. (number)]]
local function UnitBuffTime(unit,spellID,filter)
	local _,_,_,_,_,_,expire = UnitBuffID(unit, spellID, filter)
	return expire and expire - GetTime() or 0
end
--[[ stacks = UnitBuffStacks("unit", spellID[, "filter"])
	@param	unit - A unit to query (string, unitID)
	@param	spellID - Numeric ID of a spell (number)
	@param	filter - list of filters to use separated by the pipe '|' character; e.g. "CANCELABLE|PLAYER" will query cancelable debuffs cast by the player (string)
	@Return	stacks - stack count of buff, 0 otheriwse. (number)]]
local function UnitBuffStacks(unit,spellID,filter)
	local _,_,_,s = UnitBuffID(unit, spellID, filter)
	return s and s or 0
end
--[[ time = BloodlustTime()
	@Return	time - Time left on bloodlust, 0 otherwise. (number) ]]
local function BloodlustTime()
	for i=1,#LUST_BUFFS do
		local buff,_,_,_,_,_,expire = UnitBuffID("PLAYER",LUST_BUFFS[i])
		if buff then return expire - GetTime()	end
	end
	return 0
end
-- PARTY/RAID ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
--[[ unit = GetTank(role)
	@params 	role - Role to search group for (string)
		TANK - Return maintank Unit
		ASSIST - Return mainassist Unit
	@return 	unit - unit if assigned the role, nil otherise (string, unitID)]]
local function GetTank(role)
	local groupCount = GetNumGroupMembers()
	if groupCount and groupCount > 0 then
		if IsInRaid() then
			if role == 'ASSIST' then
				local assist -- offtank in LFR
				for i=1, groupCount do
					if GetPartyAssignment('MAINTANK','raid'..i) then assist = 'raid'..i end -- In LFR both tanks are assigned MAINTANK, so this will catch the second tank, iff not in LFR this will just return the MAINTANK iff no MAINASSIST is set.
					if GetPartyAssignment('MAINASSIST','raid'..i) then return 'raid'..i end
				end
				return assist
			else
				for i=1, groupCount do
					if GetPartyAssignment('MAINTANK','raid'..i) then return 'raid'..i end
				end
			end
		else
			for i=1, groupCount do
				if UnitGroupRolesAssigned('party'..i) == role then return 'party'..i end
			end			
		end
	end
end
-- DEATHKNIGHT ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
--[[ blood, frost, unholy, death = GetRunes()
	@Return
		blood - Number of ready blood runes. [0-2] (number)
		frost - Number of ready frost runes. [0-2] (number)
		unholy - Number of ready unholy runes. [0-2] (number)
		death	- Number of ready death runes. [0-2] (number) ]]
local function GetRunes()
	local blood, frost, unholy, death = 0,0,0,0
	local type
	for i=1,6 do
		type 	=  GetRuneType(i)
		if  select(3, GetRuneCooldown(i)) then -- ready
			if type == 1 then
				blood = blood + 1
			elseif type == 2 then
				unholy = unholy + 1
			elseif type == 3 then
				frost = frost + 1
			elseif type == 4 then
				death = death + 1
			end
		end
	end
	return blood, frost, unholy, death
end
--[[ depleted = GetDepletedRunes()
	@Return	depleted	- number of fully delpleted runes [0-3] (number) ]]
local function GetDepletedRunes()
	local regenThreshold  = 1
	local depleted = 0
	for i=1,6,2 do
		local start, duration, runeReady = GetRuneCooldown(i)
		local start2, duration2, runeReady2 = GetRuneCooldown(i+1)
		if not runeReady and not runeReady2 and duration > 0 and duration2 > 0 and start > 0 and start2 > 0 then
			if (start-GetTime()+duration)>=regenThreshold and (start2-GetTime()+duration2)>=regenThreshold then
				depleted = depleted + 1
			end
		end
	end
	return depleted
end

-- ENGINE ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
--[[ isHotkeys = IsHotkeys(hotkeyTable)
	@param	hotkeyTable	Table of hotkeys to check for
	@Return	isHotkeys	True if hotkey combination pressed or no hotkeys passed in via the hotkeyTable, nil otherwise (bool) ]]
local function IsHotkeys(hotkeyTable)
	if not hotkeyTable then return false end
	if hotkeyTable.strict then
		local keyTotal 		= 0
		local hotkeyTotal 	= 0
		if IsLeftShiftKeyDown()		then keyTotal = keyTotal + 1 	end
		if IsLeftControlKeyDown()	then keyTotal = keyTotal + 2 	end
		if IsLeftAltKeyDown() 		then keyTotal = keyTotal + 4 	end
		if IsRightShiftKeyDown()	then keyTotal = keyTotal + 8 	end
		if IsRightControlKeyDown()	then keyTotal = keyTotal + 16 end
		if IsRightAltKeyDown() 		then keyTotal = keyTotal + 32 end
		for k=1, #hotkeyTable do hotkeyTotal = hotkeyTotal + KEY_VALUES[hotkeyTable[k]] end
		return hotkeyTotal == 0 and true or hotkeyTotal == keyTotal
	else
		for k=1, #hotkeyTable do if not KEYS[hotkeyTable[k]]() then return false end end
		return true
	end
end
--[[ SetLagTolerance([ms])
	@param	percent			Set the time before a Global Cooldown has finished to start sending the next action. Only applicable to actions on the GCD, if nil then set back to default:300ms (number) ]]
local function SetLagTolerance(ms)
	if type(ms) ~= 'number' then
		lagTolerance = .2 -- reset
	return end
	lagTolerance = ms/1000
end
--[[ success = CastAction("actionName" [, spellID, "unit", recastDelay, cancelChannel, noDeadCheck])
	@param	actionName		Short descriptive name for your action (string)
	@param	spellID			Numeric ID of spell you want to cast (number)
	@param	Unit				target for your spell (string, UnitID)
	@param	recastDelay		Set a delay for your spell, triggered on a succesful cast (number)
	@param	cancelChannel	allow this action to interupt a channeled spell
	@param	silent			Dont log this action (useful for abilities that have no choice but to spam)
	@param	noDeathCheck	bypass UnitIsDead check
	@Return	success			True if spell met casting requirements and was cast, false otherwise. ]]
local function CastAction(actionName, spellID, unit, recastDelay, cancelChannel, silent, noDeadCheck)
	if not spellID then ADDON:Debug("rotEngine",actionName.." has no spellID, relying on rotation conditional lua.") return true end

	local spellIndex, spellBook = spellBook:GetSpellIndex(spellID)
	local spellname = GetSpellInfo(spellID)
	if not spellIndex then return false	end
	local mouse

	if unit and unit:lower() == 'mouse' then unit = nil mouse = true end
	if unit and ( not UnitExists(unit) or not UnitIsVisible(unit) ) then	return false end
	if unit and not noDeadCheck and UnitIsDead(unit) then return false end
	if spellBook then
		if spellBook == BOOKTYPE_SPELL then
			if unit and SpellHasRange(spellIndex, spellBook) == 1 then
				if IsSpellInRange(spellIndex, spellBook, unit) == 0 then return false end
				if IsHarmfulSpell(spellIndex, spellBook) and not UnitCanAttack("player", unit) then return false end
			end
		elseif spellBook == BOOKTYPE_PET then
			if not UnitExists('pet') or SpellChanneling('pet') then return false end
		end
	else
		spellID = spellIndex
		spellname = GetSpellInfo(spellID)
		if unit and SpellHasRange(spellname) == 1 then 
			if IsSpellInRange(spellname, unit) == 0 then return false end
			if IsHarmfulSpell(spellname) and not UnitCanAttack("player", unit) then return false end
		end
	end

	if SpellChanneling('player') and not cancelChannel then return false end
	if not SpellAvailable(spellID) then return false end
	if spellRecastDelay[spellID] and spellRecastDelay[spellID] >= (GetTime() * 1000)	then return false end

	-- attempt to cast the spell if	
	spellRecast[spellID] = recastDelay or 0

	if not silent then ADDON.Engine:LogAction(actionName,spellID) end

	if unit then
		ADDON:Debug("rotEngine","Casting "..actionName.." ("..spellname..") on "..UnitName(unit))
		CastSpellByName(spellname, unit)
	else
		if mouse then
			ADDON:Debug("rotEngine","Casting "..actionName.." ("..spellname..") on mouse")
			CameraOrSelectOrMoveStart(1)
      CastSpellByName(spellname)
      CameraOrSelectOrMoveStop(1)
		else
			ADDON:Debug("rotEngine","Casting "..actionName.." ("..spellname..")  ")
			CastSpellByName(spellname)
		end
	end

	return true
	-- spellId == 116694 spellId == 124682 spellId == 123273
end

ADDON.API = {
	SCRIPTPATH				= ADDON.SCRIPTPATH,
	Error							= ADDON.Error,
	
	FaceUnit					= FaceUnit,
	GetDistance				= GetDistance,
	
	GetUnitID					= GetUnitID,
	
	InCombat					= InCombat,
	IsBoss 						= IsBoss,	
	
	TalentActive			= TalentActive,
	SpellInRange 			= SpellInRange,
	SpellAvailable 		= SpellAvailable,
	SpellCooldownTime	= SpellCooldownTime,	
	SpellChanneling 	= SpellChanneling,	
	
	UnitBuffID 				= UnitBuffID,
	UnitBuffTime 			= UnitBuffTime,
	UnitBuffStacks 		= UnitBuffStacks,
	UnitDebuffID 			= UnitDebuffID,	
	UnitDebuffTime 		= UnitDebuffTime,	
	UnitDebuffStacks 	= UnitDebuffStacks,
	BloodlustTime			= BloodlustTime,
	
	GetTank 					= GetTank,
	
	GetRunes 					= GetRunes,
	GetDepletedRunes 	= GetDepletedRunes,
	
	IsHotkeys					= IsHotkeys,
	SetLagTolerance		= SetLagTolerance,
	CastAction 				= CastAction,
}
